// TODO: CODE REVIEW & CLEANUP!
//
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.ServiceModel;
using System.Text;
using System.Windows.Media.Imaging;
using System.Xml;
using HomeUX.MusicServer;
using System.ServiceModel.Description;

class Program : IMusicWcfServiceCallback
{
    const string DefaultServerUrl = "net.tcp://localhost:3535/HomeUX.MusicServer";

    string _serverUrl = DefaultServerUrl;
    IMusicWcfService _musicServer;
	bool _waitingForCallbacks;

    static void Main(string[] args)
    {
        new Program(args);
    }

    public Program(string[] args)
    {
        bool forceClose = false;
        try
        {
            IEnumerator<string> argEnum = ((IEnumerable<string>)args).GetEnumerator();
            while (argEnum.MoveNext())
            {
                ArtistInfo[] artists;
                AlbumInfo[] albums;
                AlbumDetails albumDetails;
                string artistName, albumId;
                int trackNumber;
                MusicServiceSettings settings;
                string playlistName;
                if (argEnum.Current == new String('.', argEnum.Current.Length))
                    System.Threading.Thread.Sleep(1000 * argEnum.Current.Length);
                else
                switch (argEnum.Current)
                {

                case "-serverUrl":

                    _serverUrl = GetNextArgument(argEnum);
                    OpenConnection();
                    break;

                case "-getArtists":

                    OpenConnection();
                    artists =_musicServer.GetArtists();
                    foreach (ArtistInfo artist in artists)
                        Console.WriteLine(artist.Name);
                    break;

                case "-getAlbumsOfArtist":

                    // e.g.: -getAlbumsOfArtist "Sarah McLachlan"
                    OpenConnection();
                    artistName = GetNextArgument(argEnum);
                    albums = _musicServer.GetAlbumsOfArtist(artistName, 0);
                    if (albums.Length == 0)
                        Console.WriteLine("No albums");
                    else
                    {
                        Console.WriteLine("Album Id: Album Title");
                        foreach (AlbumInfo album in albums)
                            Console.WriteLine("{0}: {1}", album.Id, album.Name);
                    }
                    break;

                case "-getAlbumDetails":

                    // e.g.: -getAlbumDetails "Surfacing*;*Sarah McLachlan"
                    OpenConnection();
                    albumId = GetNextArgument(argEnum);
                    albumDetails = _musicServer.GetAlbumDetails(albumId);
                    Console.WriteLine(albumDetails.ArtistName);
                    Console.WriteLine(albumDetails.Name);
                    Console.WriteLine("Track Number: Track Title (Duration)");
                    foreach (TrackDetails track in albumDetails.Tracks)
                    {
                        int seconds = (int)track.Duration;
                        Console.WriteLine("{0,4}: {1} ({2}:{3:00})",
                            track.TrackNumber, track.Name, seconds / 60, seconds % 60);
                    }
                    break;

                case "-search":

                    // e.g.: -search "Sarah"
                    OpenConnection();
                    string find = GetNextArgument(argEnum);
                    var request = new SearchRequest()
                    {
                        Find = find,
                        MaxArtists = int.MaxValue,
                        MaxAlbums = int.MaxValue,
                        MaxTracks = int.MaxValue
                    };
                    SearchResult result = _musicServer.Search(request);
                    Console.WriteLine("Found {0} artists, {1} albums, {2} tracks",
                        result.FoundArtistCount, result.FoundAlbumCount, result.FoundTrackCount);

                    Console.WriteLine("----- Found {0} Artists -----", result.FoundArtistCount);
                    foreach (FoundArtist foundArtist in result.FoundArtists)
                    {
                        Console.WriteLine("    \"{0}\"", foundArtist.ArtistName);
                    }

                    Console.WriteLine("----- Found {0} Albums -----", result.FoundAlbumCount);
                    foreach (FoundAlbum foundAlbum in result.FoundAlbums)
                    {
                        Console.WriteLine("    \"{0}\": {1}: {2}", foundAlbum.AlbumId,
                            foundAlbum.ArtistName, foundAlbum.AlbumName);
                    }

                    Console.WriteLine("----- Found {0} Tracks -----", result.FoundTrackCount);
                    foreach (FoundTrack foundTrack in result.FoundTracks)
                    {
                        Console.WriteLine("    \"{0}\" {1}: {2}: {3}: {4}",
                            foundTrack.TrackId.AlbumId, foundTrack.TrackId.TrackNumber,
							foundTrack.ArtistName, foundTrack.AlbumName, foundTrack.TrackName);
                    }
                    break;

                case "-getQueueDetails":

                    OpenConnection();
                    QueueDetails queueDetails = _musicServer.GetQueueDetails();
                    if (queueDetails.Tracks.Length == 0)
                    {
                        Console.WriteLine("Queue is empty");
                    }
                    else
                    {
                        foreach (AlbumTrackDetails track in queueDetails.Tracks)
                        {
							Console.WriteLine("\"{0}\" {1}", track.AlbumId,
								track.TrackDetails.TrackNumber);
                        }
                    }
                    break;

                case "-playAlbum":

                    // e.g.: -playAlbum "Surfacing*;*Sarah McLachlan" 7
                    OpenConnection();
                    albumId = GetNextArgument(argEnum);
                    trackNumber = GetNextIntArgument(argEnum);
                    _musicServer.PlayAlbum(albumId, trackNumber);
                    break;

                case "-addToQueue":

                    // e.g.: -addToQueue "Surfacing*;*Sarah McLachlan" 7
                    OpenConnection();
                    albumId = GetNextArgument(argEnum);
                    trackNumber = GetNextIntArgument(argEnum);
                    _musicServer.AddToQueue(new TrackId[] { NewTrackId(albumId, trackNumber) });
                    break;

                case "-removeFromQueue":

                    // e.g.: -removeFromQueue "Surfacing*;*Sarah McLachlan" 7
                    OpenConnection();
                    albumId = GetNextArgument(argEnum);
                    trackNumber = GetNextIntArgument(argEnum);
                    _musicServer.RemoveFromQueue(new TrackId[] { NewTrackId(albumId, trackNumber) });
                    break;

                case "-clearQueue":

                    OpenConnection();
                    _musicServer.ClearQueue();
                    break;

                case "-getPlaylists":

                    OpenConnection();
                    PlaylistInfo[] playlists = _musicServer.GetPlaylists();
                    foreach (PlaylistInfo playlist in playlists)
                        Console.WriteLine(playlist.Name);
                    break;

                case "-loadPlaylist":

                    // e.g.: -loadPlaylist "My Favorite Songs"
                    OpenConnection();
                    playlistName = GetNextArgument(argEnum);
                    _musicServer.LoadPlaylist(playlistName);
                    break;

                case "-saveQueue":

                    // e.g.: -saveQueue "My Favorite Songs"
                    OpenConnection();
                    playlistName = GetNextArgument(argEnum);
                    _musicServer.SaveQueue(playlistName);
                    break;

                case "-deletePlaylist":

                    // e.g.: -deletePlaylist "My Favorite Songs"
                    OpenConnection();
                    playlistName = GetNextArgument(argEnum);
                    _musicServer.DeletePlaylist(playlistName);
                    break;

                case "-play":

                    OpenConnection();
                    _musicServer.Play();
                    break;

                case "-pause":

                    OpenConnection();
                    _musicServer.Pause();
                    break;

                case "-stop":

                    OpenConnection();
                    _musicServer.Stop();
                    break;

                case "-fastForward":

                    OpenConnection();
                    _musicServer.FastForward();
                    break;

                case "-fastReverse":

                    OpenConnection();
                    _musicServer.FastReverse();
                    break;

                case "-nextTrack":

                    OpenConnection();
                    _musicServer.NextTrack();
                    break;

                case "-previousTrack":

                    OpenConnection();
                    _musicServer.PreviousTrack();
                    break;

                case "-playQueueItem":

                    // e.g.: -playQueueItem "Surfacing*;*Sarah McLachlan" 7
                    OpenConnection();
                    albumId = GetNextArgument(argEnum);
                    trackNumber = GetNextIntArgument(argEnum);
                    _musicServer.PlayQueueItem(NewTrackId(albumId, trackNumber));
                    break;

                case "-getSettings":

                    OpenConnection();
                    settings = _musicServer.GetSettings();
                    Console.WriteLine("Volume IsMuted PlaybackRate IsLoopMode IsShuffleMode");
                    Console.WriteLine("{0,6} {1,7} {2,12:n2} {3,10} {4,13}",
                        settings.Volume, settings.IsMuted, settings.PlaybackRate,
                        settings.IsLoopMode, settings.IsShuffleMode);
                    break;

                case "-setSettings":

                    OpenConnection();
                    settings = new MusicServiceSettings();
                    settings.Volume = GetNextIntArgument(argEnum);
                    settings.IsMuted = GetNextBoolArgument(argEnum);
                    settings.PlaybackRate = GetNextDoubleArgument(argEnum);
                    settings.IsLoopMode = GetNextBoolArgument(argEnum);
                    settings.IsShuffleMode = GetNextBoolArgument(argEnum);
                    _musicServer.SetSettings(settings);
                    break;

                case "-getDetailedStatus":

                    OpenConnection();
                    MusicServiceDetailedStatus status = _musicServer.GetDetailedStatus();
                    Console.WriteLine("PlayState: {0}", status.PlayState);
                    Console.WriteLine("AlbumId: {0}", status.AlbumId);
                    Console.WriteLine("TrackNumber: {0}", status.TrackNumber);
                    Console.WriteLine("Position: {0}", status.Position);
                    Console.WriteLine("QueueIndex: {0}", status.QueueIndex);
                    if (status.AlbumArt != null)
                    {
                        Size size = GetAlbumArtSize(status.AlbumArt);
                        Console.WriteLine("AlbumArt: {0}x{1} pixels ({2} bytes)",
                            size.Width, size.Height, status.AlbumArt.Length);
                    }
                    else
                        Console.WriteLine("AlbumArt: (none)");
                    break;

                case "-exportLibrary":

                    // e.g.: -exportLibrary IncludeArtists.txt Library.xml
                    OpenConnection();
                    artists = _musicServer.GetArtists();
                    string includeArtistsFileName = GetNextArgument(argEnum);
                    string[] lines = File.ReadAllLines(includeArtistsFileName);
                    var includeArtists = new HashSet<string>();
                    foreach (string line in lines)
                        includeArtists.Add(line.Trim());
                    string libraryFileName = GetNextArgument(argEnum);
                    int mostAlbumsPerArtist = -1;
                    string artistWithMostAlbums = null;
                    var xmlWriterSettings = new XmlWriterSettings();
                    xmlWriterSettings.Indent = true;
                    xmlWriterSettings.Encoding = Encoding.UTF8;
                    //xmlWriterSettings.OmitXmlDeclaration = true;
                    using (XmlWriter writer = XmlWriter.Create(libraryFileName, xmlWriterSettings))
                    {
                        writer.WriteStartElement("Artists");
                        for (int artistIndex = 0; artistIndex < artists.Length; artistIndex++)
                        {
                            ArtistInfo artist = artists[artistIndex];
                            bool included = includeArtists.Remove(artist.Name);
                            Console.WriteLine("{0}: Artist {1} of {2}: {3}",
								(included ? "Included" : "Excluded"), artistIndex + 1,
								artists.Length, artist.Name);
                            if (!included)
                                continue;
                            writer.WriteStartElement("Artist");
                            writer.WriteAttributeString("Name", artist.Name);
                            writer.WriteAttributeString("SortKey", artist.SortKey);
                            albums = _musicServer.GetAlbumsOfArtist(artist.Name, 0);
                            if (mostAlbumsPerArtist < albums.Length)
                            {
                                mostAlbumsPerArtist = albums.Length;
                                artistWithMostAlbums = artist.Name;
                            }
                            foreach (AlbumInfo album in albums)
                            {
                                writer.WriteStartElement("Album");
                                writer.WriteAttributeString("Id", album.Id);
                                writer.WriteAttributeString("Name", album.Name);
                                albumDetails = _musicServer.GetAlbumDetails(album.Id);
                                if (albumDetails.AlbumArt != null)
                                {
                                    Size size = GetAlbumArtSize(albumDetails.AlbumArt);
                                    writer.WriteAttributeString("AlbumArt",
                                        String.Format("{0};{1};{2}",
                                            XmlConvert.ToString(size.Width),
                                            XmlConvert.ToString(size.Height),
                                            XmlConvert.ToString(albumDetails.AlbumArt.Length)));
                                }
                                foreach (TrackDetails track in albumDetails.Tracks)
                                {
                                    writer.WriteStartElement("Track");
									writer.WriteAttributeString("TrackNumber",
										XmlConvert.ToString(track.TrackNumber));
									writer.WriteAttributeString("Name", track.Name);
									writer.WriteAttributeString("Duration",
										XmlConvert.ToString(track.Duration));
									writer.WriteAttributeString("Year",
										XmlConvert.ToString(track.Year));
									writer.WriteAttributeString("Composer", track.Composer);
                                    writer.WriteEndElement(); // "Track"
                                }
                                writer.WriteEndElement(); // "Album"
                            }
                            writer.WriteEndElement(); // "Artist"
                        }
                        writer.WriteEndElement(); // "Artists"
                    }
                    if (artistWithMostAlbums != null)
                    {
                        Console.Error.WriteLine(
							"Done.  Artist with most albums: {0} ({1} album(s))",
                            artistWithMostAlbums, mostAlbumsPerArtist);
                    }
                    foreach (string name in includeArtists)
                        Console.Error.WriteLine("Artist not found: {0}", name);
                    break;

                case "-wait":

                    OpenConnection();
                    Console.WriteLine("Listening for events - press any key to exit...");
					_waitingForCallbacks = true;
                    Console.ReadKey();
                    break;

                default:

                    throw new UsageException();
                }
            }
        }
        catch (UsageException)
        {
            Console.Error.WriteLine(
                "Usage: MusicTestClient <options>\n" +
                "\n" +
                "Tests HomeUX.MusicServer.exe.  <options> may include:\n" +
                "-serverUrl <url>\n" +
                "    Specifies the server URL.  The default is:\n" +
                "    {0}\n" +
                "-getArtists\n" +
                "    Lists the name of each artist.\n" +
                "-getAlbumsOfArtist <artist-name>\n" +
                "    Lists the album IDs and names of a given artist.\n" +
                "-getAlbumDetails <album-id>\n" +
                "    Outputs information about a given album.\n" +
                "-search <find>\n" +
                "    Searches the music library for the specified string.\n" +
                "-getQueueDetails\n" +
                "    Outputs information about the \"now playing\" queue (playlist).\n" +
                "-playAlbum <album-id> <starting-track-number>\n" +
                "    Clears the queue and starts the given track playing.\n" +
                "-addToQueue <album-id> <track-number>\n" +
                "    Queues the given track to play after whatever's currently playing.\n" +
                "-removeFromQueue <album-id> <track-number>\n" +
                "    Removes the given track from the queue (if present).\n" +
                "-clearQueue\n" +
                "    Empties the queue.\n" +
                "-getPlaylists\n" +
                "    Lists the name of each playlist.\n" +
                "-loadPlaylist <playlist-name>\n" +
                "    Loads the specified playlist into the queue.\n" +
                "-savePlaylist <playlist-name>\n" +
                "    Saves the queue to the specified playlist.\n" +
                "-play\n" +
                "    Performs the same action as pressing the Play button in Media Player.\n" +
                "-pause\n" +
                "    Performs the same action as pressing the Pause button in Media Player.\n" +
                "-stop\n" +
                "    Performs the same action as pressing the Stop button in Media Player.\n" +
                "-fastForward\n" +
                "    Starts the current track playing in fast-forward mode.\n" +
                "-fastReverse\n" +
                "    Starts the current track playing in fast-reverse mode.\n" +
                "-nextTrack\n" +
                "    Moves to the next track in the queue.\n" +
                "-previousTrack\n" +
                "    Moves to the previous track in the queue.\n" +
                "-playQueueItem <queue-item-index>\n" +
                "    Starts playing at a given queue item, given its zero-based index.\n" +
                "-getSettings\n" +
                "    Displays current settings.\n" +
                "-setSettings <volume> <muted> <playback-rate> <loop-mode> <shuffle-mode>\n" +
                "    Sets current settings.  <volume> is an integer 0-100; <muted>,\n" +
                "    <loop-mode>, and <shuffle-mode> are \"True\" or \"False\";\n" +
                "    <playback-rate> is floating point.\n" +
                "-getDetailedStatus\n" +
                "    Gets detailed status information.\n" +
                "-exportLibrary <include-artists> <output-file>\n" +
                "    Exports in XML to <output-file> the artists named (one per line) in file\n" +
				"    <include-artists> (use -getArtists for an example of this format).\n" +
                "-wait\n" +
                "    Waits until a key is pressed.  Displays events from the server.\n" +
                ". or .. or ... etc.\n" +
                "    Sleeps for N seconds before the next <option>, where N = no. periods\n",
                DefaultServerUrl);

            //
        }
        catch (Exception ex)
        {
            // note that FaultException derives from CommunicationException
            if ((ex is EndpointNotFoundException) || (ex is CommunicationException) ||
                (ex is TimeoutException) || (ex is IOException) ||
				(ex is UnauthorizedAccessException))
            {
                Console.Error.WriteLine(ex.Message);
                forceClose = true;
            }
            else
                throw;
        }
        finally
        {
            CloseConnection(forceClose);
        }
    }

    // IMusicWcfServiceCallback implementation...

    public void StatusChanged(MusicServiceStatus status)
    {
        // to avoid confusion, ignore callbacks unless -wait was specified
        if (!_waitingForCallbacks)
            return;

        // display information from the callback
        Console.WriteLine("Status: {0}: {1}: {2}[{3}]: {4}",
            status.PlayState, status.AlbumId, status.TrackNumber, status.QueueIndex,
            status.Position);
    }

    public void QueueChanged(QueueDetails queueDetails)
    {
        // to avoid confusion, ignore callbacks unless -wait was specified
        if (!_waitingForCallbacks)
            return;

        // display information from the callback
        Console.WriteLine("Queue changed -- {0} tracks:", queueDetails.Tracks.Length);
        foreach (AlbumTrackDetails track in queueDetails.Tracks)
            Console.WriteLine("\"{0}\" {1}", track.AlbumId, track.TrackDetails.TrackNumber);
    }

    public void LibraryChanged()
    {
        Console.WriteLine("Library changed");
    }

    public void SettingsChanged(MusicServiceSettings settings)
    {
        Console.WriteLine("Settings changed:");
        Console.WriteLine("  Volume = {0}", settings.Volume.Value);
        Console.WriteLine("  IsMuted = {0}", settings.IsMuted.Value);
        Console.WriteLine("  PlaybackRate = {0}", settings.PlaybackRate.Value);
        Console.WriteLine("  IsLoopMode = {0}", settings.IsLoopMode.Value);
        Console.WriteLine("  IsShuffleMode = {0}", settings.IsShuffleMode.Value);
    }

    // Helper methods...

    void OpenConnection()
    {
        if (_musicServer == null)
        {
            var factory = new DuplexChannelFactory<IMusicWcfService>(this, "Callback");
            factory.Endpoint.Address = new EndpointAddress(_serverUrl);
            _musicServer = factory.CreateChannel();

            // force the connection open so we start receiving events; note that
            // "((IClientChannel)_musicServer).Open()" doesn't suffice
            _musicServer.Hello();
        }
    }

    void CloseConnection(bool force)
    {
        if (_musicServer != null)
        {
            if (!force)
            {
                try
                {
                    _musicServer.Goodbye();
                }
                catch (CommunicationException)
                {
                }
            }
            try
            {
                ((IClientChannel)_musicServer).Close();
            }
            catch (CommunicationException)
            {
            }
            ((IDisposable)_musicServer).Dispose();
            _musicServer = null;
        }
    }

    Size GetAlbumArtSize(byte[] albumArt)
    {
        using (var stream = new MemoryStream(albumArt))
        {
            var image = new BitmapImage();
            image.BeginInit();
            image.StreamSource = stream;
            image.EndInit();
            return new Size()
            {
                Width = image.PixelWidth,
                Height = image.PixelHeight
            };
        }
    }

    static string GetNextArgument(IEnumerator<string> argEnum)
    {
        if (!argEnum.MoveNext())
            throw new UsageException();
        return argEnum.Current;
    }

    static int GetNextIntArgument(IEnumerator<string> argEnum)
    {
        string stringValue = GetNextArgument(argEnum);
        try
        {
            return int.Parse(stringValue);
        }
        catch (Exception ex)
        {
            if ((ex is FormatException) || (ex is OverflowException))
                throw new UsageException();
            else
                throw;
        }
    }

    static double GetNextDoubleArgument(IEnumerator<string> argEnum)
    {
        string stringValue = GetNextArgument(argEnum);
        try
        {
            return double.Parse(stringValue);
        }
        catch (Exception ex)
        {
            if ((ex is FormatException) || (ex is OverflowException))
                throw new UsageException();
            else
                throw;
        }
    }

    static bool GetNextBoolArgument(IEnumerator<string> argEnum)
    {
        string stringValue = GetNextArgument(argEnum);
        try
        {
            return bool.Parse(stringValue);
        }
        catch (Exception ex)
        {
            if ((ex is FormatException) || (ex is OverflowException))
                throw new UsageException();
            else
                throw;
        }
    }

    TrackId NewTrackId(string albumId, int trackNumber)
    {
        return new TrackId()
        {
            AlbumId = albumId,
            TrackNumber = trackNumber
        };
    }
}

class UsageException : Exception
{
}
